Repository navigation
Conversation
…ookups
A label lookup with match[] filtered the tags table by `id IN (timeSeriesSelector(...))`.
The tags table is ordered by metric name, so that predicate alone reads every tags row.
When every selector names its metric (`name{...}`, `name` or `{__name__="name"}`), also
filter on metric_name: the same rows, but only those metrics' part of the table is read.
|
@bcanvural is attempting to deploy a commit to the HyperDX Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: 71e2aa6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| export function selectorMetricName(selector: string): string | undefined { | ||
| const bare = /^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(\{|$)/.exec(selector); | ||
| if (bare) return bare[1]; | ||
| const label = /^\s*\{(?:[^}]*,)?\s*__name__\s*=\s*"((?:[^"\\]|\\.)*)"/.exec( |
There was a problem hiding this comment.
🔵 minor — Regex can read a __name__ inside another label's value, so the lookup silently returns nothing
The prefix (?:[^}]*,)? matches any text before a comma, including text inside a quoted label value. Take {path="/a,__name__=",env="prod"}: the prefix matches path="/a,, the closing quote of path is taken as the opening quote of __name__, and the function returns ,env=. The query then adds metric_name IN (',env='), so the lookup returns no values when it should return every series with that path. The same happens to any label value containing ,__name__= that is followed by another quoted matcher. A related gap: escape sequences are returned as written, not decoded (\\ stays as two characters). Fix: walk the matchers with a small tokenizer that skips over quoted strings, and return undefined if the __name__ value contains a backslash. Add a test for the comma case. The existing {job="__name__=\"up\""} test has no comma before __name__, so it never reaches this path.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
There was a problem hiding this comment.
Fixed in b7c7613. Matchers before __name__ are now matched whole, including single-quoted and backtick values, and only a plain metric name is taken. The unit tests cover both quoting styles.
PR Review
1 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 1 minor 1 posted as inline comment(s) on the changed lines. Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
Deep Review✅ No critical issues found. This is a tightly-scoped, well-iterated performance change. The new 🟡 P2 -- recommended
Reviewers (8): correctness, adversarial, performance, testing, maintainability, previous-comments, kieran-typescript, project-standards. Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
The metric name was found by skipping everything before `__name__`, which could land inside a single-quoted or backtick value and narrow the lookup to the wrong metric. Match whole label matchers before it, and only take a plain metric name.
|
b7c7613 fixes the Not changing:
|
|
|
||
| // Whole matchers before `__name__`, so a quoted value containing | ||
| // `__name__="x"` isn't read as one. Names that need escaping fall back. | ||
| const NAME_MATCHER = |
There was a problem hiding this comment.
🔵 minor — Hand-rolled regex PromQL selector parser duplicates the lezer-promql parser already used in common-utils
Move metric-name extraction into common-utils next to injectPromqlLabelMatchers (packages/common-utils/src/core/promqlLabelInjection.ts), which already parses with @prometheus-io/lezer-promql. Walk the VectorSelector to get its name, or its single __name__= matcher or quoted name. That handles comments, Prometheus 3 quoted names ({"my.metric"}) and escaped values, which the NAME_MATCHER regex skips. This follows the REQUIRED 'grep before adding a helper' rule.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
There was a problem hiding this comment.
Not changing here: that parser lives in common-utils, and using it means a new export from a package this PR doesn't touch. The regex accepts only the three forms and falls back to the plain id lookup on anything else. Happy to switch if you'd prefer.
There was a problem hiding this comment.
Hi @bcanvural can we create a module that uses @prometheus-io/lezer-promql in common-utils to handle this? And then call it here instead of adding this regex
There was a problem hiding this comment.
Done in a34014d: promqlSelectorMetricName in common-utils/src/core/promqlSelector.ts walks the lezer-promql tree, and the regex is gone. It returns a name only for a single vector selector with a metric identifier or an __name__="..." matcher, and undefined otherwise (including parse errors), which keeps the plain id lookup.
| }); | ||
| }); | ||
|
|
||
| describe('selectorMetricName', () => { |
There was a problem hiding this comment.
🔵 minor — {__name__="…"} selectors that add the new metric_name IN filter are never run end-to-end
Add cases to timeseriesEngine.int.test.ts with match: ['{__name__="recent_metric", job="api"}'] and a mix of __name__-form and bare selectors. Assert the same values as the equivalent recent_metric{job="api"} case. Right now only the unit test of selectorMetricName checks the NAME_MATCHER branch, and nothing runs its SQL against ClickHouse.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
There was a problem hiding this comment.
Not changing: {__name__="up"} produces the same metric_name IN filter as up, which the match[] selectors integration tests run end to end. The unit tests cover the parsing.
|
On the latest review, not changing: |
|
|
||
| // Whole matchers before `__name__`, so a quoted value containing | ||
| // `__name__="x"` isn't read as one. Names that need escaping fall back. | ||
| const NAME_MATCHER = |
There was a problem hiding this comment.
Hi @bcanvural can we create a module that uses @prometheus-io/lezer-promql in common-utils to handle this? And then call it here instead of adding this regex
Use @prometheus-io/lezer-promql in a common-utils module instead of a regex in the API, as asked in review.
| const names = match | ||
| .map(promqlSelectorMetricName) | ||
| .filter((n): n is string => n != null); | ||
| if (names.length < match.length) return ids; |
There was a problem hiding this comment.
🔵 minor — No test covers the new metric_name IN (...) clause or its fallback
Add a unit test on getSeriesSelectorCondition (or on the SQL that queryLabelValues/queryLabelNames builds) for three cases: all selectors named, which should add the metric_name filter with deduped names; one unnamed selector mixed in, which should leave only id IN (...); and two different named selectors, which should give metric_name IN (a, b). The existing timeseriesEngine.int.test.ts cases check only that results don't change, so they would still pass if the filter were never emitted (for example with an inverted names.length < match.length check), and they never exercise the mixed named/unnamed fallback.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
There was a problem hiding this comment.
Added in 71e2aa6: labelValues for job with ['{job="web"}', 'old_metric'] expects ['batch', 'web'], and fails if a mixed list is narrowed to the named metric. I said earlier that the labelNames test covered this. It didn't: web's label names also appear on old_metric series.
Not adding a test on the generated SQL: the filter changes only speed, not results, and the parser's cases are unit-tested.
| const names = match | ||
| .map(promqlSelectorMetricName) | ||
| .filter((n): n is string => n != null); | ||
| if (names.length < match.length) return ids; |
There was a problem hiding this comment.
🔵 minor — No test checks that the metric_name IN (...) filter is ever added, so the speed-up could vanish with every test still passing
By design the filter returns the same rows, so the integration tests in timeseriesEngine.int.test.ts give identical results whether it is added or not. If getSeriesSelectorCondition always returned ids, or the names.length < match.length check were flipped, every test would still pass. The only new integration test covers the case where no filter is added. Also, no integration test runs a braced {__name__="x"} selector through ClickHouse. Fix: export getSeriesSelectorCondition (or move it into a small module) and add unit tests on the generated ChSql. Check that the SQL contains metric_name IN with the deduplicated names when every selector names its metric (bare, name{...} and {__name__="x"} forms), and that it is left out when any selector does not name one.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Fixes #3602. Follow-up to #3581.
What
A PromQL label lookup with
match[]filters the tags table byid IN (timeSeriesSelector(...)). The tags table is ordered by metric name, so that alone reads every tags row. When every selector names its metric (name{...},name, or{__name__="name", ...}), the lookup now also filters onmetric_name IN (...): the same rows, but only those metrics' part of the table is read. Selectors that may match several metrics keep today's behaviour.Why
On a TimeSeries table with about 140M series (ClickHouse 26.10, schema version 8), a dropdown scoped to a ~3k-series metric took 6–16 s; with the metric name filter 0.24 s, same result (numbers in #3602).
Tests
selectorMetricNamefor bare, braced and__name__selectors, and for selectors that may match several metrics (regex, negative matcher,__name__inside a label value).timeseriesEngine.int.test.tsmatch[]cases (45/45 pass locally) cover the unchanged results.